# -*- coding: utf-8 -*-

import calendar
import datetime
import re

month_mappings = {"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6, "JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10,
                  "NOW": 11, "DEC": 12}

re_year = "(?P<years>(19[789]\d{1}|[2-9]\d{3}|\*)(/[1-9]\d*)?|(19[789]\d{1}|[2-9]\d{3})-(19[789]\d{1}|[2-9]\d{3})|(19[789]\d{1}|[2-9]\d{3})(-(19[789]\d{1}|[2-9]\d{3}))?(,(19[789]\d{1}|[2-9]\d{3})(-(19[789]\d{1}|[2-9]\d{3}))?)+)"
re_month = "(?P<months>((0?[1-9]|1[012])|\*)(/[1-9]\d*)?|(0?[1-9]|1[012])-(0?[1-9]|1[012])|(0?[1-9]|1[012])(-(0?[1-9]|1[012]))?(,(0?[1-9]|1[012])(-(0?[1-9]|1[012]))?)+|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC)-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC))?(,(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOW|DEC))?)+)"
re_day = "(?P<days>((0?[1-9]|[12]\d|3[01])|\*)(/[1-9]\d*)?|(0?[1-9]|[12]\d|3[01])-(0?[1-9]|[12]\d|3[01])|(0?[1-9]|[12]\d|3[01])(-(0?[1-9]|[12]\d|3[01]))?(,(0?[1-9]|[12]\d|3[01])(-(0?[1-9]|[12]\d|3[01]))?)+)"
re_hour = "(?P<hours>(([01]?\d|2[0123])|\*)(/[1-9]\d*)?|([01]?\d|2[0123])-([01]?\d|2[0123])|([01]?\d|2[0123])(-([01]?\d|2[0123]))?(,([01]?\d|2[0123])(-([01]?\d|2[0123]))?)+)"
re_minute = "(?P<minutes>([0-5]?\d|\*)(/[1-9]\d*)?|[0-5]?\d-[0-5]?\d|[0-5]?\d(-[0-5]?\d)?(,[0-5]?\d(-[0-5]?\d)?)+)"
re_second = "(?P<seconds>([0-5]?\d|\*)(/[1-9]\d*)?|[0-5]?\d-[0-5]?\d|[0-5]?\d(-[0-5]?\d)?(,[0-5]?\d(-[0-5]?\d)?)+)"

cron_expression = "^%s\s+%s\s+%s\s+%s\s+%s\s+%s$" % (re_year, re_month, re_day, re_hour, re_minute, re_second)

re_range = ".*[-,].*"
re_star = "\*"
re_forward_flash = "/"

min_year, min_month, min_day, min_hour, min_minute, min_second = 1970, 1, 1, 0, 0, 0
max_year, max_month, max_day, max_hour, max_minute, max_second = 10000, 13, 32, 24, 60, 60


class CronError(Exception):

    def __init__(self, error_msg):
        self.error_msg = error_msg

    def __str__(self):
        return repr(self.error_msg)


class Cron:

    def __init__(self, cron, init_datetime=datetime.datetime.now()):
        self.__init_year = init_datetime.year
        self.__init_month = init_datetime.month
        self.__init_day = init_datetime.day
        self.__init_hour = init_datetime.hour
        self.__init_minute = init_datetime.minute
        self.__init_second = init_datetime.second

        match = re.match(cron_expression, cron)
        if match is not None:
            years = match.group("years")
            self.__years = self.__resolve(years, min_year, max_year)

            months = match.group("months")
            for key, value in month_mappings.items():
                months = months.replace(key, str(value))
            self.__months = self.__resolve(months, min_month, max_month)

            days = match.group("days")
            self.__days = self.__resolve(days, min_day, max_day)

            hours = match.group("hours")
            self.__hours = self.__resolve(hours, min_hour, max_hour)

            minutes = match.group("minutes")
            self.__minutes = self.__resolve(minutes, min_minute, max_minute)

            seconds = match.group("seconds")
            self.__seconds = self.__resolve(seconds, min_second, max_second)
        else:
            raise CronError('Cron Expression Format Error')

    def __resolve(self, field_value, min_value, max_value):
        if re.match(re_range, field_value):
            points = self.__flatten(field_value)
        else:
            values = re.split(re_forward_flash, field_value)
            if re.match(re_star, field_value):
                period = 1 if len(values) == 1 else int(values[1])
                points = [i for i in range(min_value, max_value, period)]
            else:
                period = None if len(values) == 1 else int(values[1])
                if period is None:
                    points = [int(values[0])]
                else:
                    points = [i for i in range(int(values[0]), max_value, period)]
        return points

    def __flatten(self, ranges=""):
        values = []
        for r in [re.split("-", i) for i in re.split(",", ranges)]:
            if len(r) == 2:
                values += [i for i in range(int(r[0]), int(r[1]) + 1)]
            elif len(r) == 1:
                values.append(int(r[0]))
        return sorted(set(values))

    def next_execute_time(self):
        for year in self.__years:
            if year < self.__init_year:
                continue
            for month in self.__months:
                if year <= self.__init_year and month < self.__init_month:
                    continue
                current_max_day = calendar.monthrange(year, month)[1] + 1
                days = [i for i in filter(lambda x: x < current_max_day, self.__days)]
                for day in days:
                    if year <= self.__init_year and month <= self.__init_month and day < self.__init_day:
                        continue
                    for hour in self.__hours:
                        if year <= self.__init_year and month <= self.__init_month and day <= self.__init_day and hour < self.__init_hour:
                            continue
                        for minute in self.__minutes:
                            if year <= self.__init_year and month <= self.__init_month and day <= self.__init_day and hour <= self.__init_hour and minute < self.__init_minute:
                                continue
                            for second in self.__seconds:
                                if year <= self.__init_year and month <= self.__init_month and day <= self.__init_day and hour <= self.__init_hour and minute <= self.__init_minute and second < self.__init_second:
                                    continue
                                yield datetime.datetime(year, month, day, hour, minute, second)
